/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */

import { strict as assert } from "node:assert";

import { oob, unreachableCase } from "@fluidframework/core-utils/internal";
import { MockHandle, validateUsageError } from "@fluidframework/test-runtime-utils/internal";

import { TreeStatus } from "../../../feature-libraries/index.js";
import {
	type ObjectNodeSchema,
	SchemaFactoryAlpha,
	treeNodeApi as Tree,
	TreeViewConfiguration,
	type TreeArrayNode,
	type TreeMapNode,
	type TreeView,
	typeSchemaSymbol,
	type NodeFromSchema,
	type TreeNodeFromImplicitAllowedTypes,
	type TreeNodeSchema,
	type WithType,
	isTreeNode,
	NodeKind,
	type TreeFieldFromImplicitField,
	TreeViewConfigurationAlpha,
	SchemaFactoryBeta,
	allowUnused,
	type InsertableTreeFieldFromImplicitField,
} from "../../../simple-tree/index.js";
import {
	// Import directly to get the non-type import to allow testing of the package only instanceof
	TreeNode,
	type AllowedTypes,
	type AllowedTypesFull,
	type AnnotatedAllowedType,
	type AnnotatedAllowedTypes,
	type InsertableTreeNodeFromAllowedTypes,
	type InsertableTreeNodeFromImplicitAllowedTypes,
	type UnannotateAllowedTypesList,
	// eslint-disable-next-line import-x/no-internal-modules
} from "../../../simple-tree/core/index.js";
import {
	SchemaFactory,
	schemaFromValue,
	// eslint-disable-next-line import-x/no-internal-modules
} from "../../../simple-tree/api/schemaFactory.js";
import {
	schemaStaticsStable,
	type SchemaStatics,
	// eslint-disable-next-line import-x/no-internal-modules
} from "../../../simple-tree/api/schemaStatics.js";
import {
	brand,
	type areSafelyAssignable,
	type IsUnion,
	type requireAssignableTo,
	type requireFalse,
	type requireTrue,
} from "../../../util/index.js";

import { hydrate } from "../utils.js";
import { getView, TestTreeProviderLite } from "../../utils.js";
import type { SchematizingSimpleTreeView } from "../../../shared-tree/index.js";
import { EmptyKey } from "../../../core/index.js";

// Tests for the non-recursive API subset of SchemaFactory and SchemaFactoryAlpha.
// Recursive APIs are tested in schemaFactoryRecursive.spec.ts

{
	const schema = new SchemaFactory("Blah");

	class Note extends schema.object("Note", { text: schema.string }) {}

	class NodeMap extends schema.map("Notes", Note) {}
	class NodeList extends schema.array("Notes", Note) {}

	// eslint-disable-next-line no-inner-declarations
	function f(n: NodeMap): void {
		const item = n.get("x");
	}

	// Leaf stuff
	{
		const x = schema.string;
		type _check = requireAssignableTo<typeof schema.string, TreeNodeSchema>;
	}

	// TreeNodeFromImplicitAllowedTypes
	{
		type _check = requireAssignableTo<typeof Note, TreeNodeSchema>;
		type Test = TreeNodeFromImplicitAllowedTypes<typeof Note>;
		type Instance = InstanceType<typeof Note>;
		type _check2 = requireTrue<areSafelyAssignable<Test, Note>>;

		type _check3 = requireTrue<
			areSafelyAssignable<TreeNodeFromImplicitAllowedTypes<[typeof Note]>, Note>
		>;
		type _check4 = requireTrue<
			areSafelyAssignable<TreeNodeFromImplicitAllowedTypes<[() => typeof Note]>, Note>
		>;

		type FromArray = TreeNodeFromImplicitAllowedTypes<[typeof Note, typeof Note]>;
		type _check5 = requireTrue<areSafelyAssignable<FromArray, Note>>;
	}
	// TreeNodeFromImplicitAllowedTypes with a class
	{
		class NoteCustomized extends schema.object("Note", { text: schema.string }) {
			public test: boolean = false;
		}

		type _check = requireAssignableTo<typeof NoteCustomized, TreeNodeSchema>;
		type _checkNodeType = requireAssignableTo<
			typeof NoteCustomized,
			TreeNodeSchema<string, NodeKind, NoteCustomized>
		>;

		type Instance = InstanceType<typeof NoteCustomized>;
		type _checkInstance = requireTrue<areSafelyAssignable<Instance, NoteCustomized>>;

		type Test = TreeNodeFromImplicitAllowedTypes<typeof NoteCustomized>;
		type _check2 = requireTrue<areSafelyAssignable<Test, NoteCustomized>>;

		type _check3 = requireTrue<
			areSafelyAssignable<
				TreeNodeFromImplicitAllowedTypes<[typeof NoteCustomized]>,
				NoteCustomized
			>
		>;
		type _check4 = requireTrue<
			areSafelyAssignable<
				TreeNodeFromImplicitAllowedTypes<[() => typeof NoteCustomized]>,
				NoteCustomized
			>
		>;

		type FromArray = TreeNodeFromImplicitAllowedTypes<
			[typeof NoteCustomized, typeof NoteCustomized]
		>;
		type _check5 = requireTrue<areSafelyAssignable<FromArray, NoteCustomized>>;
	}

	// TreeFieldFromImplicitField
	{
		type _check = requireAssignableTo<typeof Note, TreeNodeSchema>;
		type Test = TreeFieldFromImplicitField<typeof Note>;
		type Instance = InstanceType<typeof Note>;
		type _check2 = requireTrue<areSafelyAssignable<Test, Note>>;

		type _check3 = requireTrue<
			areSafelyAssignable<TreeFieldFromImplicitField<[typeof Note]>, Note>
		>;
		type _check4 = requireTrue<
			areSafelyAssignable<TreeFieldFromImplicitField<[() => typeof Note]>, Note>
		>;

		type FromArray = TreeFieldFromImplicitField<[typeof Note, typeof Note]>;
		type _check5 = requireTrue<areSafelyAssignable<FromArray, Note>>;
	}
}

describe("schemaFactory", () => {
	it("leaf", () => {
		const schema = new SchemaFactory("com.example");

		const config = new TreeViewConfiguration({ schema: schema.number });

		const view = getView(config);
		view.initialize(5);
		assert.equal(view.root, 5);
	});

	it("instanceof", () => {
		const schema = new SchemaFactory("com.example");

		class A extends schema.object("A", {}) {}
		class B extends schema.object("B", {}) {}
		const C = schema.object("C", {});
		const StructuralArray = schema.array(A);
		const NominalArray = schema.array("D", A);

		const a = new A({});
		assert(a instanceof A);
		assert(a instanceof TreeNode);
		assert(!(a instanceof B));

		// @ts-expect-error Nodes should get type based nominal typing.
		const b: A = new B({});

		const c = new C({});
		assert(c instanceof C);
		assert(c instanceof TreeNode);
		assert(!(c instanceof B));

		const n = new NominalArray([]);
		assert(n instanceof NominalArray);
		assert(n instanceof TreeNode);
		assert(!(n instanceof B));

		// Structurally typed and/or POJO mode types:
		const s = hydrate(StructuralArray, []);
		// This works correctly, but is currently rejected by the type system. This is fine as Tree.is can be used instead.
		assert(s instanceof (StructuralArray as never));
		// This case is expressible without type errors, so it is important that it works.
		assert(s instanceof TreeNode);
		assert(!(s instanceof B));
	});

	it("Scoped", () => {
		const factory = new SchemaFactory("test-scope");
		// We specified a scope in the factory, so it should be part of the type signature of the created object
		const foo = factory.object("foo", {}).identifier;
		type _check = requireTrue<areSafelyAssignable<"test-scope.foo", typeof foo>>;
		assert.equal(foo, "test-scope.foo");
	});

	it("Unscoped", () => {
		const factory = new SchemaFactory(undefined);
		// We did not specify a scope in the factory, so one should not be part of the type signature of the created object
		const foo = factory.object("foo", {}).identifier;
		type _check = requireTrue<areSafelyAssignable<"foo", typeof foo>>;
		assert.equal(foo, "foo");
	});

	it("Optional fields", () => {
		const factory = new SchemaFactory("test");
		class Foo extends factory.object("foo", {
			x: factory.optional(factory.number),
		}) {}

		const _check1 = new Foo({});
		const _check2 = new Foo({ x: undefined });
		const _check3 = new Foo({ x: 1 });
	});

	it("empty field", () => {
		// A field with no allowed types and thus must always be empty.
		const config = new TreeViewConfiguration({ schema: SchemaFactory.optional([]) });
		const view = getView(config);
		view.initialize(undefined);
		assert.equal(view.root, undefined);
		type Field = typeof view.root;
		type _check = requireTrue<areSafelyAssignable<Field, undefined>>;
	});

	it("empty object field", () => {
		const factory = new SchemaFactory("test-scope");
		class Foo extends factory.object("foo", {
			// A field with no allowed types and thus must always be empty.
			x: SchemaFactory.optional([]),
		}) {}

		const config = new TreeViewConfiguration({ schema: Foo });
		const view = getView(config);
		view.initialize({});
		assert.equal(view.root.x, undefined);
		type Field = typeof view.root.x;
		type _check = requireTrue<areSafelyAssignable<Field, undefined>>;
	});

	it("Required fields", () => {
		const factory = new SchemaFactory("test");
		class Foo extends factory.object("foo", {
			x: factory.required(factory.number),
		}) {}

		assert.throws(
			() => {
				// @ts-expect-error Missing required field
				const _check1 = new Foo({});
			},
			validateUsageError(/incompatible/),
		);

		assert.throws(
			() => {
				// @ts-expect-error Required field cannot be undefined
				const _check2 = new Foo({ x: undefined });
			},
			validateUsageError(/incompatible/),
		);

		const _check3 = new Foo({ x: 1 });
	});

	// Regression test to ensure generic type variations of the factory are assignable to its default typing.
	it("Typed factories are assignable to default typing", () => {
		type _check1 = requireTrue<
			requireAssignableTo<SchemaFactory<"Foo", "Bar">, SchemaFactory>
		>;
		type _check2 = requireTrue<requireAssignableTo<SchemaFactory<"Foo", 42>, SchemaFactory>>;
		type _check3 = requireTrue<
			requireAssignableTo<SchemaFactory<undefined, "Bar">, SchemaFactory>
		>;
		type _check4 = requireTrue<
			requireAssignableTo<SchemaFactory<undefined, 42>, SchemaFactory>
		>;
	});

	describe("object", () => {
		it("simple end to end", () => {
			const schema = new SchemaFactory("com.example");
			class Point extends schema.object("Point", {
				x: schema.number,
				y: schema.number,
			}) {}

			const config = new TreeViewConfiguration({ schema: Point });

			const view = getView(config);
			view.initialize(new Point({ x: 1, y: 2 }));
			const { root } = view;
			assert.equal(root.x, 1);
			assert.equal(root.y, 2);

			const values: number[] = [];
			Tree.on(root, "nodeChanged", () => {
				values.push(root.x);
			});
			root.x = 5;
			assert.equal(root.x, 5);
			assert.deepEqual(values, [5]);
		});

		it("custom members", () => {
			const schema = new SchemaFactory("com.example");
			class Point extends schema.object("Point", {
				x: schema.number,
			}) {
				public selected = false;

				public toggle(): boolean {
					this.selected = !this.selected;
					return this.selected;
				}

				public increment(): number {
					return this.x++;
				}
			}

			const config = new TreeViewConfiguration({ schema: Point });

			const view = getView(config);
			view.initialize(new Point({ x: 1 }));
			const { root } = view;
			assert.equal(root.x, 1);

			const values: number[] = [];
			Tree.on(root, "nodeChanged", () => {
				values.push(root.x);
			});

			assert(root instanceof Point);
			assert(isTreeNode(root));
			assert(Reflect.has(root, "selected"));
			assert.equal(root.selected, false);
			// Ensure modification works
			root.selected = true;
			assert.equal(root.selected, true);
			// Ensure methods work
			assert.equal(root.toggle(), false);
			// Ensure methods and direct access observe same property.
			assert.equal(root.selected, false);

			// Ensure methods can access tree content
			assert.equal(root.increment(), 1);
			assert.equal(root.increment(), 2);
			assert.deepEqual(values, [2, 3]);
		});

		it("Stored key collision", () => {
			const schema = new SchemaFactory("com.example");
			assert.throws(
				() =>
					schema.object("Point", {
						x: schema.required(schema.number, { key: "foo" }),
						y: schema.required(schema.number, { key: "foo" }),
					}),
				validateUsageError(/Duplicate stored key "foo" in schema "com.example.Point"/),
			);
		});

		it("Stored key collides with property key", () => {
			const schema = new SchemaFactory("com.example");
			assert.throws(
				() =>
					schema.object("Object", {
						foo: schema.number,
						bar: schema.required(schema.string, { key: "foo" }),
					}),

				validateUsageError(
					/Stored key "foo" in schema "com.example.Object" conflicts with a property key of the same name/,
				),
			);
		});

		// This is a somewhat neurotic test case, and likely not something we would expect a user to do.
		// But just in case, we should ensure it is handled correctly.
		it("Stored key / property key swap", () => {
			const schema = new SchemaFactory("com.example");
			assert.doesNotThrow(() =>
				schema.object("Object", {
					foo: schema.optional(schema.number, { key: "bar" }),
					bar: schema.required(schema.string, { key: "foo" }),
				}),
			);
		});

		it("Explicit stored key === property key", () => {
			const schema = new SchemaFactory("com.example");
			assert.doesNotThrow(() =>
				schema.object("Object", {
					foo: schema.optional(schema.string, { key: "foo" }),
				}),
			);
		});

		it("Node schema metadata - beta", () => {
			const factory = new SchemaFactoryBeta("");

			const fooMetadata = {
				description: "An object called Foo",
				custom: {
					baz: true,
				},
			} as const;

			class Foo extends factory.object(
				"Foo",
				{ bar: factory.number },
				{ metadata: fooMetadata },
			) {}

			// Ensure `Foo.metadata` is typed as we expect, and we can access its fields without casting.
			const description = Foo.metadata.description;
			type _check1 = requireTrue<areSafelyAssignable<typeof description, string | undefined>>;

			const custom = Foo.metadata.custom;

			// Currently it is impossible to have required custom metadata: it always includes undefined as an option.
			// TODO: having a way to make metadata required would be nice.

			type _check2 = requireTrue<
				areSafelyAssignable<typeof custom, { baz: true } | undefined>
			>;
			assert(custom !== undefined);

			const baz = custom.baz;
			type _check3 = requireTrue<areSafelyAssignable<typeof baz, true>>;

			// This must be checked after the types are checked to avoid it narrowing and making the type checks above not test anything.
			assert.deepEqual(Foo.metadata, fooMetadata);
		});

		it("Node schema metadata - alpha", () => {
			const factory = new SchemaFactoryAlpha("");

			const fooMetadata = {
				description: "An object called Foo",
				custom: {
					baz: true,
				},
			} as const;

			class Foo extends factory.objectAlpha(
				"Foo",
				{ bar: factory.number },
				{ metadata: fooMetadata },
			) {}

			// Ensure `Foo.metadata` is typed as we expect, and we can access its fields without casting.
			const description = Foo.metadata.description;
			type _check1 = requireTrue<areSafelyAssignable<typeof description, string | undefined>>;

			const custom = Foo.metadata.custom;

			// Currently it is impossible to have required custom metadata: it always includes undefined as an option.
			// TODO: having a way to make metadata required would be nice.

			type _check2 = requireTrue<
				areSafelyAssignable<typeof custom, { baz: true } | undefined>
			>;
			assert(custom !== undefined);

			const baz = custom.baz;
			type _check3 = requireTrue<areSafelyAssignable<typeof baz, true>>;

			// This must be checked after the types are checked to avoid it narrowing and making the type checks above not test anything.
			assert.deepEqual(Foo.metadata, fooMetadata);
		});

		it("Field schema metadata", () => {
			const schemaFactory = new SchemaFactory("com.example");
			const barMetadata = {
				description: "Bar",
				custom: { prop1: "Custom metadata property." },
			};

			class Foo extends schemaFactory.object("Foo", {
				bar: schemaFactory.required(schemaFactory.number, { metadata: barMetadata }),
			}) {}

			const foo = hydrate(Foo, { bar: 37 });

			const schema = Tree.schema(foo) as ObjectNodeSchema;
			// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
			assert.deepEqual(schema.fields.get("bar")!.metadata, barMetadata);
		});

		it("Node schema persisted metadata", () => {
			const factory = new SchemaFactoryAlpha("com.example");

			const fooMetadata = { "a": 2 };

			class Foo extends factory.objectAlpha(
				"Foo",
				{ bar: factory.number },
				{ persistedMetadata: fooMetadata },
			) {}

			assert.deepEqual(Foo.persistedMetadata, fooMetadata);
		});

		it("Field schema persisted metadata", () => {
			const schemaFactory = new SchemaFactoryAlpha("com.example");
			const fooMetadata = { "a": 2 };

			class Foo extends schemaFactory.objectAlpha(
				"Foo",
				{
					bar: schemaFactory.required(schemaFactory.number, {
						persistedMetadata: fooMetadata,
					}),
					baz: schemaFactory.optional(schemaFactory.string, {
						persistedMetadata: fooMetadata,
					}),
					qux: schemaFactory.optionalRecursive(
						schemaFactory.objectAlpha("Qux", { quux: schemaFactory.string }),
						{ persistedMetadata: fooMetadata },
					),
				},
				{ persistedMetadata: fooMetadata },
			) {}

			const foo = hydrate(Foo, {
				bar: 37,
				baz: "test",
				qux: { quux: "test" },
			});

			const schema = Tree.schema(foo) as ObjectNodeSchema;
			// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
			assert.deepEqual(schema.fields.get("bar")!.persistedMetadata, fooMetadata);
			// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
			assert.deepEqual(schema.fields.get("baz")!.persistedMetadata, fooMetadata);
			// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
			assert.deepEqual(schema.fields.get("qux")!.persistedMetadata, fooMetadata);
		});

		describe("deep equality", () => {
			const schema = new SchemaFactory("com.example");

			class Item extends schema.object("Item", {
				x: schema.number,
				y: schema.number,
			}) {}
			class Point extends schema.object("Point", {
				x: schema.number,
				y: schema.number,
			}) {}
			it("hydrated", () => {
				assert.deepEqual(hydrate(Point, { x: 1, y: 2 }), hydrate(Point, { x: 1, y: 2 }));
				// It should not matter if the object was unhydrated or just builder data:
				assert.deepEqual(
					hydrate(Point, new Point({ x: 1, y: 2 })),
					hydrate(Point, { x: 1, y: 2 }),
				);
				assert.notDeepEqual(hydrate(Point, { x: 1, y: 2 }), hydrate(Point, { x: 1, y: 3 }));
				assert.notDeepEqual(hydrate(Point, { x: 1, y: 2 }), { x: 1, y: 2 });
				assert.notDeepEqual(hydrate(Point, { x: 1, y: 2 }), hydrate(Item, { x: 1, y: 2 }));
			});

			it("local fields", () => {
				class WithLocals extends schema.object("WithLocals", {
					x: schema.number,
				}) {
					public extra = true;
					public method(): void {}
				}
				const p1 = hydrate(WithLocals, { x: 1 });
				const p2 = hydrate(WithLocals, { x: 1 });
				assert.deepEqual(p1, p2);
				p1.extra = false;
				assert.notDeepEqual(p1, p2);
			});

			it("unhydrated", () => {
				assert.deepEqual(new Point({ x: 1, y: 2 }), new Point({ x: 1, y: 2 }));
				assert.notDeepEqual(new Point({ x: 1, y: 2 }), new Point({ x: 1, y: 3 }));
				assert.notDeepEqual(new Point({ x: 1, y: 2 }), { x: 1, y: 2 });
				assert.notDeepEqual(new Point({ x: 1, y: 2 }), hydrate(Item, { x: 1, y: 2 }));
			});
		});
	});

	it("mixed", () => {
		const schema = new SchemaFactory("com.example");

		class Point extends schema.object("Point", {
			x: schema.number,
			y: schema.number,
		}) {}

		class Note extends schema.object("Note", {
			text: schema.string,
			location: schema.optional(Point),
		}) {}

		class NodeMap extends schema.map("NoteMap", Note) {}
		class NodeList extends schema.array("NoteList", Note) {}

		class Canvas extends schema.object("Canvas", { stuff: [NodeMap, NodeList] }) {}

		const config = new TreeViewConfiguration({ schema: Canvas });

		const view: TreeView<typeof Canvas> = getView(config);
		view.initialize(
			new Canvas({
				stuff: new NodeList([new Note({ text: "hi", location: undefined })]),
			}),
		);
		const stuff = view.root.stuff;
		assert(stuff instanceof NodeList);
		const item = stuff[0] ?? oob();
		const s: string = item.text;
		assert.equal(s, "hi");
	});

	describe("Array", () => {
		it("Nested Array", () => {
			const builder = new SchemaFactory("test");

			class Inventory extends builder.object("Inventory", {
				parts: builder.array(builder.number),
			}) {}

			const treeConfiguration = new TreeViewConfiguration({ schema: Inventory });

			const view = getView(treeConfiguration);
			view.initialize(
				new Inventory({
					parts: [1, 2],
				}),
			);
		});

		it("Structural", () => {
			const factory = new SchemaFactory("test");

			// Explicit structural example
			const MyList = factory.array(factory.number);
			type MyList = NodeFromSchema<typeof MyList>;

			// Inline structural example
			factory.object("Foo", { myList: factory.array(factory.number) });

			function broken() {
				// @ts-expect-error structural list schema are not typed as classes.
				class NotAClass extends factory.array(factory.number) {}
			}

			assert.equal(MyList.identifier, `test.Array<["${factory.number.identifier}"]>`);
		});

		it("Named", () => {
			const factory = new SchemaFactory("test");
			class NamedList extends factory.array("name", factory.number) {
				public testProperty = false;
			}

			// Due to missing unhydrated list support, make a wrapper object
			class Parent extends factory.object("parent", { child: NamedList }) {}

			// Due to lack of support for navigating unhydrated nodes, create an actual tree so we can navigate to the list node:
			const treeConfiguration = new TreeViewConfiguration({ schema: Parent });
			const view = getView(treeConfiguration);
			view.initialize(new Parent({ child: [5] }));

			const listNode = view.root.child;
			assert(listNode instanceof NamedList);
			assert(isTreeNode(listNode));
			assert(Reflect.has(listNode, "testProperty"));
			assert.equal(listNode.testProperty, false);
			listNode.testProperty = true;
			assert.equal(listNode.testProperty, true);

			// Test method from list
			assert.equal(listNode.at(0), 5);
		});

		it("Unhydrated", () => {
			const factory = new SchemaFactory("test");
			class NamedList extends factory.array("name", factory.number) {}
			const namedInstance = new NamedList([5]);
		});

		it("Node schema metadata", () => {
			const factory = new SchemaFactoryAlpha("");

			const fooMetadata = {
				description: "An array of numbers",
				custom: {
					baz: true,
				},
			};

			class Foo extends factory.arrayAlpha("Foo", factory.number, { metadata: fooMetadata }) {}

			assert.deepEqual(Foo.metadata, fooMetadata);

			// Ensure `Foo.metadata` is typed as we expect, and we can access its fields without casting.
			const description = Foo.metadata.description;
			const baz = Foo.metadata.custom.baz;
			type _check1 = requireTrue<areSafelyAssignable<typeof description, string>>;
			type _check2 = requireTrue<areSafelyAssignable<typeof baz, boolean>>;
		});
	});

	describe("Map", () => {
		it("Structural", () => {
			const factory = new SchemaFactory("test");

			// Explicit structural example
			const MyMap = factory.map(factory.number);
			type MyMap = NodeFromSchema<typeof MyMap>;

			// Inline structural example
			factory.object("Foo", { myMap: factory.map(factory.number) });

			function broken() {
				// @ts-expect-error structural map schema are not typed as classes.
				class NotAClass extends factory.map(factory.number) {}
			}
		});

		it("Named", () => {
			const factory = new SchemaFactory("test");
			class NamedMap extends factory.map("name", factory.number) {
				public testProperty = false;
			}

			// Due to missing unhydrated map support, make a wrapper object
			class Parent extends factory.object("parent", { child: NamedMap }) {}

			// Due to lack of support for navigating unhydrated nodes, create an actual tree so we can navigate to the map node:
			const treeConfiguration = new TreeViewConfiguration({ schema: Parent });
			const view = getView(treeConfiguration);
			view.initialize(new Parent({ child: new Map([["x", 5]]) }));

			const mapNode = view.root.child;
			assert(mapNode instanceof NamedMap);
			assert(isTreeNode(mapNode));
			assert(Reflect.has(mapNode, "testProperty"));
			assert.equal(mapNode.testProperty, false);
			mapNode.testProperty = true;
			assert.equal(mapNode.testProperty, true);

			// Test method from map
			assert.equal(mapNode.get("x"), 5);
		});

		it("Unhydrated", () => {
			const factory = new SchemaFactory("test");
			class NamedMap extends factory.map("name", factory.number) {}
			const namedInstance = new NamedMap(new Map([["x", 5]]));
		});

		it("Node schema metadata", () => {
			const factory = new SchemaFactoryAlpha("");

			const fooMetadata = {
				description: "A map of numbers",
				custom: {
					baz: true,
				},
			};

			class Foo extends factory.mapAlpha("Foo", factory.number, { metadata: fooMetadata }) {}

			assert.deepEqual(Foo.metadata, fooMetadata);

			// Ensure `Foo.metadata` is typed as we expect, and we can access its fields without casting.
			const description = Foo.metadata.description;
			const baz = Foo.metadata.custom.baz;
			type _check1 = requireTrue<areSafelyAssignable<typeof description, string>>;
			type _check2 = requireTrue<areSafelyAssignable<typeof baz, boolean>>;
		});
	});

	describe("Record", () => {
		it("Structural", () => {
			const factory = new SchemaFactoryAlpha("test");

			// Explicit structural example
			const MyRecord = factory.record(factory.number);
			type MyRecord = NodeFromSchema<typeof MyRecord>;

			// Inline structural example
			factory.object("Foo", { myMap: factory.record(factory.number) });

			function broken() {
				// @ts-expect-error structural map schema are not typed as classes.
				class NotAClass extends factory.record(factory.number) {}
			}
		});

		it("Named", () => {
			const factory = new SchemaFactoryAlpha("test");
			class NamedRecord extends factory.record("name", factory.number) {}

			// Due to missing unhydrated map support, make a wrapper object
			class Parent extends factory.object("parent", { child: NamedRecord }) {}

			// Due to lack of support for navigating unhydrated nodes, create an actual tree so we can navigate to the map node:
			const treeConfiguration = new TreeViewConfiguration({ schema: Parent });
			const view = getView(treeConfiguration);
			view.initialize(new Parent({ child: { x: 5 } }));

			const recordNode = view.root.child;
			assert(recordNode instanceof NamedRecord);
			assert(isTreeNode(recordNode));

			// Test record property access
			assert.equal(recordNode.x, 5);
			recordNode.x = 42;
			assert.equal(recordNode.x, 42);
		});

		it("Unhydrated", () => {
			const factory = new SchemaFactoryAlpha("test");
			class NamedRecord extends factory.record("name", factory.number) {}
			const namedInstance = new NamedRecord({ x: 5 });
			const x: number = namedInstance.x;
			// TODO: AB#47136: this (and likely the line above as well) should not compile as the typing is incorrect (y is undefined, not number)
			const y: number = namedInstance.y;
			delete namedInstance.x;
		});

		it("Node schema metadata", () => {
			const factory = new SchemaFactoryAlpha("");

			const fooMetadata = {
				description: "A map of numbers",
				custom: {
					baz: true,
				},
			};

			class Foo extends factory.recordAlpha("Foo", factory.number, {
				metadata: fooMetadata,
			}) {}

			assert.deepEqual(Foo.metadata, fooMetadata);

			// Ensure `Foo.metadata` is typed as we expect, and we can access its fields without casting.
			const description = Foo.metadata.description;
			const baz = Foo.metadata.custom.baz;
			type _check1 = requireTrue<areSafelyAssignable<typeof description, string>>;
			type _check2 = requireTrue<areSafelyAssignable<typeof baz, boolean>>;
		});
	});

	describe("produces proxies that can be read after insertion for trees of", () => {
		// This suite ensures that proxies created via `new X(...)` are "hydrated" after they are inserted into the tree.
		// After insertion, each of those proxies should be the same object as the corresponding proxy in the tree.

		// This schema allows trees of all the various combinations of containers.
		// For example, "objects with lists", "lists of maps", "maps of lists", "lists of lists", etc.
		// It will be used below to generate test cases of the various combinations.
		const comboSchemaFactory = new SchemaFactory("combo");
		class ComboChildObject extends comboSchemaFactory.object("comboObjectChild", {}) {}
		class ComboChildList extends comboSchemaFactory.array(
			"comboListChild",
			comboSchemaFactory.null,
		) {}
		class ComboChildMap extends comboSchemaFactory.map(
			"comboMapChild",
			comboSchemaFactory.null,
		) {}
		class ComboParentObject extends comboSchemaFactory.object("comboObjectParent", {
			childA: [ComboChildObject, ComboChildList, ComboChildMap],
			childB: [ComboChildObject, ComboChildList, ComboChildMap],
		}) {}
		class ComboParentList extends comboSchemaFactory.array("comboListParent", [
			ComboChildObject,
			ComboChildList,
			ComboChildMap,
		]) {}
		class ComboParentMap extends comboSchemaFactory.map("comboMapParent", [
			ComboChildObject,
			ComboChildList,
			ComboChildMap,
		]) {}
		class ComboRoot extends comboSchemaFactory.object("comboRoot", {
			root: [ComboParentObject, ComboParentList, ComboParentMap],
		}) {}

		type ComboParent = ComboParentObject | ComboParentList | ComboParentMap;
		function isComboParent(value: unknown): value is ComboParent {
			return (
				value instanceof ComboParentObject ||
				value instanceof ComboParentList ||
				value instanceof ComboParentMap
			);
		}
		type ComboChild = ComboChildObject | ComboChildList | ComboChildMap;
		function isComboChild(value: unknown): value is ComboChild {
			return (
				value instanceof ComboChildObject ||
				value instanceof ComboChildList ||
				value instanceof ComboChildMap
			);
		}
		type ComboNode = ComboParent | ComboChild;

		/** Iterates through all the nodes in a combo tree */
		function* walkComboObjectTree(combo: ComboNode): IterableIterator<ComboNode> {
			yield combo;

			if (combo instanceof ComboParentObject) {
				for (const child of Object.values(combo)) {
					yield* walkComboObjectTree(child);
				}
			} else if (combo instanceof ComboParentList) {
				for (const c of combo) {
					yield* walkComboObjectTree(c);
				}
			} else if (combo instanceof ComboParentMap) {
				for (const c of combo.values()) {
					yield* walkComboObjectTree(c);
				}
			}
		}

		/** Sorts parent nodes before child nodes */
		function compareComboNodes(a: ComboNode, b: ComboNode): -1 | 0 | 1 {
			if (isComboParent(a) && isComboChild(b)) {
				return 1;
			}
			if (isComboChild(a) && isComboParent(b)) {
				return -1;
			}
			return 0;
		}

		/**
		 * Defines the structure of a combo tree.
		 * @example
		 * A layout of
		 * ```json
		 * { "parent": "list", "child": "map" }
		 * ```
		 * defines a combo tree which is a list of maps.
		 */
		interface ComboTreeLayout {
			parentType: "object" | "list" | "map";
			childType: "object" | "list" | "map";
		}

		/**
		 * Builds trees of {@link ComboObject}s according to the given {@link ComboTreeLayout}.
		 * Records all built objects and assigns each a unique ID.
		 */
		function createComboTree(layout: ComboTreeLayout) {
			const nodes: ComboNode[] = [];
			function createComboParent(): ComboParent {
				const childA = createComboChild();
				const childB = createComboChild();
				let parent: ComboParent;
				switch (layout.parentType) {
					case "object":
						parent = new ComboParentObject({ childA, childB });
						break;
					case "list":
						parent = new ComboParentList([childA, childB]);
						break;
					case "map":
						parent = new ComboParentMap(
							new Map([
								["childA", childA],
								["childB", childB],
							]),
						);
						break;
					default:
						unreachableCase(layout.parentType);
				}
				nodes.push(parent);
				assert.equal(Tree.status(parent), TreeStatus.New);
				return parent;
			}

			function createComboChild(): ComboChild {
				let child: ComboChild;
				switch (layout.childType) {
					case "object":
						child = new ComboChildObject({});
						break;
					case "list":
						child = new ComboChildList([]);
						break;
					case "map":
						child = new ComboChildMap(new Map());
						break;
					default:
						unreachableCase(layout.childType);
				}
				nodes.push(child);
				assert.equal(Tree.status(child), TreeStatus.New);
				return child;
			}

			return { parent: createComboParent(), nodes };
		}

		const objectTypes = ["object", "list", "map"] as const;
		function test(
			parentType: (typeof objectTypes)[number],
			childType: (typeof objectTypes)[number],
			validate: (view: TreeView<typeof ComboRoot>, nodes: ComboNode[]) => void,
		) {
			const config = new TreeViewConfiguration({ schema: ComboRoot });

			// Check that nodes in the initial tree are hydrated
			const view = getView(config);
			const { parent: initialParent, nodes: initialNodes } = createComboTree({
				parentType,
				childType,
			});

			view.initialize({ root: initialParent });
			validate(view, initialNodes);

			// Check that nodes inserted later are hydrated
			const { parent: insertedParent, nodes: insertedNodes } = createComboTree({
				parentType,
				childType,
			});

			// Ensure that the proxies can be read during the change, as well as after
			// Note: as of 2024-03-28, we can't easily test 'treeChanged' because it can fire at a time where the changes
			// to the tree are not visible in the listener. 'nodeChanged' only fires once we confirmed that a
			// relevant change was actually applied to the tree so the side effects this test validates already happened.
			Tree.on(view.root, "nodeChanged", () => validate(view, insertedNodes));
			view.events.on("rootChanged", () => validate(view, insertedNodes));
			view.root.root = insertedParent;
			validate(view, insertedNodes);
		}

		for (const parentType of objectTypes) {
			for (const childType of objectTypes) {
				// Generate a test for all permutations of object, list and map
				it(`${parentType} → ${childType}`, () => {
					test(parentType, childType, (view, nodes) => {
						assert(view.root.root !== undefined);
						const treeObjects = [...walkComboObjectTree(view.root.root)];
						assert.equal(treeObjects.length, nodes.length);
						// Sort the objects we built in the same way as the objects in the tree so that we can compare them below
						nodes.sort(compareComboNodes);
						treeObjects.sort(compareComboNodes);
						for (let i = 0; i < nodes.length; i++) {
							// Each raw object should be reference equal to the corresponding object in the tree.
							assert.equal(nodes[i], treeObjects[i]);
						}
					});
				});

				it(`${parentType} → ${childType} (bottom up)`, () => {
					test(parentType, childType, (_, nodes) => {
						// Sort the nodes bottom up, so that we will observe the children before demanding the parents.
						nodes.sort(compareComboNodes);
						for (let i = nodes.length - 1; i >= 0; i--) {
							const node = nodes[i];
							if (node instanceof ComboChildObject || node instanceof ComboParentObject) {
								Object.entries(node);
							} else if (node instanceof ComboChildList || node instanceof ComboParentList) {
								for (const __ of node.entries());
							} else if (node instanceof ComboChildMap || node instanceof ComboParentMap) {
								for (const __ of node.entries());
							}
							assert.equal(Tree.status(node), TreeStatus.InDocument);
						}
					});
				});
			}
		}
	});

	it("schemaFromValue", () => {
		assert.equal(schemaFromValue(1), SchemaFactory.number);
		assert.equal(schemaFromValue(""), SchemaFactory.string);
		assert.equal(schemaFromValue(null), SchemaFactory.null);
		assert.equal(schemaFromValue(new MockHandle("x")), SchemaFactory.handle);
		assert.equal(schemaFromValue(false), SchemaFactory.boolean);
	});

	it("statics", () => {
		const f = new SchemaFactory("");
		for (const [key, value] of Object.entries(schemaStaticsStable)) {
			assert.equal((SchemaFactory as unknown as Record<string, unknown>)[key], value);
			assert.equal((f as unknown as Record<string, unknown>)[key], value);
		}

		const _static: SchemaStatics = SchemaFactory;
	});

	it("extra fields in object constructor", () => {
		const f = new SchemaFactory("");

		class Empty extends f.object("C", {}) {}

		// @ts-expect-error Invalid extra field
		// TODO: this should runtime error when constructed (not just when hydrated)
		const c2 = new Empty({ x: {} });

		class NonEmpty extends f.object("C", { a: f.null }) {}

		// @ts-expect-error Invalid extra field
		// TODO: this should error when constructed (not just when hydrated)
		new NonEmpty({ a: null, b: 0 });
	});

	it("object nested implicit construction", () => {
		const f = new SchemaFactory("");

		class C extends f.object("C", {}) {
			public readonly c = "X";
		}
		class B extends f.object("B", {
			b: C,
		}) {}
		class A extends f.object("A", {
			a: B,
		}) {}

		const tree = hydrate(A, { a: { b: {} } });
		assert.equal(tree.a.b.c, "X");
	});

	describe("multiple subclass use errors", () => {
		it("mixed configuration", () => {
			const schemaFactory = new SchemaFactory("");

			const base = schemaFactory.object("Foo", {});
			class Foo extends base {}
			assert.throws(
				() => {
					const config = new TreeViewConfiguration({ schema: [Foo, base] });
				},
				validateUsageError(/same SchemaFactory generated class/),
			);
		});

		it("mixed hydrate", () => {
			const schemaFactory = new SchemaFactory("");

			const base = schemaFactory.object("Foo", {});
			class Foo extends base {}
			const other = schemaFactory.array(base);

			assert.throws(
				() => {
					const tree_B = hydrate(other, [new Foo({})]);
				},
				validateUsageError(/same SchemaFactory generated class/),
			);
		});

		it("constructing", () => {
			const schemaFactory = new SchemaFactory("");

			const base = schemaFactory.object("Foo", {});
			class Foo extends base {}

			const _1 = new base({});

			assert.throws(
				() => {
					const _2 = new Foo({});
				},
				validateUsageError(/same SchemaFactory generated class/),
			);
		});

		it("constructing reversed", () => {
			const schemaFactory = new SchemaFactory("");

			const base = schemaFactory.object("Foo", {});
			class Foo extends base {}

			const _2 = new Foo({});

			assert.throws(
				() => {
					const _1 = new base({});
				},
				validateUsageError(/same SchemaFactory generated class/),
			);
		});

		it("mixed configs", () => {
			const schemaFactory = new SchemaFactory("");
			const base = schemaFactory.object("Foo", {});
			class Foo extends base {}
			const config = new TreeViewConfiguration({ schema: base });

			assert.throws(
				() => {
					const config2 = new TreeViewConfiguration({ schema: Foo });
				},
				validateUsageError(/same SchemaFactory generated class/),
			);
		});

		it("structural types", () => {
			const schemaFactory = new SchemaFactory("");
			const base = schemaFactory.object("Foo", {});
			class Foo extends base {}
			schemaFactory.array(base);
			assert.throws(
				() => {
					schemaFactory.array(Foo);
				},
				validateUsageError(/same SchemaFactory generated class/),
			);
		});

		it("indirect configs", () => {
			const schemaFactory = new SchemaFactory("");
			const base = schemaFactory.object("Foo", {});
			class Foo extends base {}
			const config = new TreeViewConfiguration({
				schema: schemaFactory.object("x", { x: base }),
			});
			assert.throws(
				() => {
					const config2 = new TreeViewConfiguration({
						schema: schemaFactory.map("x", Foo),
					});
				},
				validateUsageError(/same SchemaFactory generated class/),
			);
		});
	});

	it("kind based narrowing", () => {
		const factory = new SchemaFactory("");

		class Obj extends factory.object("O", {}) {}
		class Arr extends factory.array("A", []) {}
		class MapNode extends factory.map("M", []) {}

		const obj = hydrate(Obj, {});
		const arr = hydrate(Arr, []);
		const mapNode = hydrate(MapNode, {});

		function f(node: TreeNode & WithType<string, NodeKind.Object>): "object";
		function f(node: TreeNode & WithType<string, NodeKind.Array>): "array";
		function f(node: TreeNode & WithType<string, NodeKind.Map>): "map";
		function f(node: TreeNode): "any";

		function f(node: TreeNode): string {
			return "nope";
		}

		// Compile time check that NodeKind based overload resolution works as expected.
		const s1: "object" = f(obj);
		const s2: "array" = f(arr);
		const s3: "map" = f(mapNode);
		const s4: "any" = f(obj as TreeNode);

		// Check runtime data:
		assert.equal(obj[typeSchemaSymbol], Obj);
		assert.equal(arr[typeSchemaSymbol], Arr);
		assert.equal(mapNode[typeSchemaSymbol], MapNode);
	});

	it("kind based narrowing example", () => {
		const factory = new SchemaFactory("");

		class Obj extends factory.object("O", { a: factory.number }) {}
		class Arr extends factory.array("A", [factory.number]) {}
		class MapNode extends factory.map("M", [factory.number]) {}

		const obj = hydrate(Obj, { a: 5 });
		const arr = hydrate(Arr, [5]);
		const mapNode = hydrate(MapNode, { x: 5 });

		assert.deepEqual(getKeys(obj), ["a"]);
		assert.deepEqual(getKeys(arr), [0]);
		assert.deepEqual(getKeys(mapNode), ["x"]);
	});

	it("structural type collision: single type", () => {
		const factory = new SchemaFactory("");
		class Child1 extends factory.object("Child", {}) {}
		class Child2 extends factory.object("Child", {}) {}

		const a = factory.array(Child1);
		// No error: this type is the same as the one above.
		assert.equal(factory.array(Child1), a);

		// Error: this type is different from the one above.
		assert.throws(
			() => {
				factory.array(Child2);
			},
			validateUsageError(/collision/),
		);

		assert.equal(factory.array([Child1]), a);
		assert.throws(
			() => {
				factory.array([Child2]);
			},
			validateUsageError(/collision/),
		);
	});

	it("structural type collision: multi type", () => {
		const factory = new SchemaFactory("");
		class Child1 extends factory.object("Child", {}) {}
		class Child2 extends factory.object("Child", {}) {}

		const a = factory.map([Child1, SchemaFactory.null]);
		assert.equal(factory.map([SchemaFactory.null, Child1]), a);
		assert.throws(
			() => {
				factory.map([Child2, SchemaFactory.null]);
			},
			validateUsageError(/collision/),
		);
	});

	it("variance with respect to scope and alpha", () => {
		// Covariant over scope
		type _check1 = requireAssignableTo<SchemaFactory<"x">, SchemaFactory<string>>;
		type _check2 = requireAssignableTo<SchemaFactoryAlpha<"x">, SchemaFactoryAlpha<string>>;

		// Still covariant when there is a "." in the scope.
		type _check3 = requireAssignableTo<SchemaFactory<"x.y">, SchemaFactory<string>>;
		type _check4 = requireAssignableTo<SchemaFactoryAlpha<"x.y">, SchemaFactoryAlpha<string>>;

		// Alpha assignable to non-alpha
		type _check5 = requireAssignableTo<SchemaFactoryAlpha<"x">, SchemaFactory<string>>;
		type _check7 = requireAssignableTo<SchemaFactoryAlpha<"x.y">, SchemaFactory<"x.y">>;

		// TODO: For some reason, alpha can not be assigned to non alpha with "." in the scope.
		// This is a known issue, and there is a note about it in the docs for `adaptEnum`.
		// @ts-expect-error Known issue
		type _check6 = requireAssignableTo<SchemaFactoryAlpha<"x.y">, SchemaFactory<string>>;

		// TODO: AB#43345
		// This error seems to be related to `objectRecursive` and:
		type _check8<Name extends string> = requireAssignableTo<
			// @ts-expect-error Known issue: https://github.com/microsoft/TypeScript/issues/61990
			`x.y.${Name}`,
			`${string}.${Name}`
		>;
	});

	it("scopedFactory", () => {
		const factory = new SchemaFactoryBeta("test.blah");

		const scopedFactory: SchemaFactoryBeta<"test.blah.scoped"> =
			factory.scopedFactory("scoped");
		assert.equal(scopedFactory.scope, "test.blah.scoped");
		type _check = requireTrue<
			areSafelyAssignable<typeof scopedFactory.scope, "test.blah.scoped">
		>;

		type Scope = typeof scopedFactory extends SchemaFactoryBeta<infer S> ? S : never;

		function inferScope<TScope extends string>(f: SchemaFactory<TScope>) {
			return f.scope;
		}

		const inferred = inferScope(scopedFactory);
		// TODO: AB#43345
		// @ts-expect-error Known issue: see "variance with respect to scope and alpha" test.
		type _check2 = requireTrue<areSafelyAssignable<typeof inferred, "test.blah.scoped">>;

		function inferScope2<TScope extends string>(
			f: SchemaFactory<TScope> | SchemaFactoryBeta<TScope>,
		) {
			return f.scope;
		}
		const inferred2 = inferScope2(scopedFactory);
		type _check3 = requireTrue<areSafelyAssignable<typeof inferred2, "test.blah.scoped">>;
	});

	it("scopedFactoryAlpha", () => {
		const factory = new SchemaFactoryAlpha("test.blah");

		const scopedFactory: SchemaFactoryAlpha<"test.blah.scoped"> =
			factory.scopedFactoryAlpha("scoped");
		assert.equal(scopedFactory.scope, "test.blah.scoped");
		type _check = requireTrue<
			areSafelyAssignable<typeof scopedFactory.scope, "test.blah.scoped">
		>;

		type Scope = typeof scopedFactory extends SchemaFactoryAlpha<infer S> ? S : never;

		function inferScope<TScope extends string>(f: SchemaFactory<TScope>) {
			return f.scope;
		}

		const inferred = inferScope(scopedFactory);
		// TODO: AB#43345
		// @ts-expect-error Known issue: see "variance with respect to scope and alpha" test.
		type _check2 = requireTrue<areSafelyAssignable<typeof inferred, "test.blah.scoped">>;

		function inferScope2<TScope extends string>(
			f: SchemaFactory<TScope> | SchemaFactoryAlpha<TScope>,
		) {
			return f.scope;
		}
		const inferred2 = inferScope2(scopedFactory);
		type _check3 = requireTrue<areSafelyAssignable<typeof inferred2, "test.blah.scoped">>;
	});

	// TODO: AB#44317: The error messages for rejecting insertions which would put a document out of schema due to staged types are poor, and should be improved.
	// Many tests here include coverage for these errors.
	describe("staged", () => {
		const schemaFactory = new SchemaFactoryAlpha("staged tests");

		class TestObject extends schemaFactory.objectAlpha("TestObject", {
			foo: SchemaFactoryAlpha.types([
				SchemaFactoryAlpha.number,
				SchemaFactoryAlpha.staged(SchemaFactoryAlpha.string),
			]),
		}) {}

		it("allows forward references", () => {
			const schemaFactoryAlpha = new SchemaFactoryAlpha("test");
			class A extends schemaFactoryAlpha.objectAlpha("A", {
				foo: SchemaFactoryAlpha.types([SchemaFactoryAlpha.staged(() => B)]),
			}) {}

			class B extends schemaFactoryAlpha.objectAlpha("B", {}) {}

			const config = new TreeViewConfiguration({
				schema: A,
			});
		});

		it("example", () => {
			const factory = new SchemaFactoryAlpha("test");
			class A extends factory.objectAlpha("A", {}) {}
			class B extends factory.objectAlpha("B", {}) {}

			// Does not support B
			const configBefore = new TreeViewConfigurationAlpha({
				schema: A,
			});

			// Supports documents with or without B
			const configStaged = new TreeViewConfigurationAlpha({
				// Adds staged support for B.
				// Currently this requires wrapping the root field with `SchemaFactoryAlpha.required`:
				// this is normally implicitly included, but is currently required while the "staged" APIs are `@alpha`.
				schema: SchemaFactoryAlpha.types([A, SchemaFactoryAlpha.staged(B)]),
			});

			// Only supports documents with A and B: can be used to upgrade schema to add B.
			const configAfter = new TreeViewConfigurationAlpha({
				schema: [A, B],
			});
		});

		describe("in objects", () => {
			it("are permitted when unhydrated", () => {
				const testObject = new TestObject({ foo: "test" });
				assert.equal(testObject.foo, "test");
				testObject.foo = 42;
				assert.equal(testObject.foo, 42);
				testObject.foo = "test";
				assert.equal(testObject.foo, "test");
			});

			it("can't be hydrated", () => {
				const testObject = new TestObject({ foo: "test" });
				const provider = new TestTreeProviderLite(1);

				const config = new TreeViewConfiguration({
					schema: TestObject,
				});
				const view = provider.trees[0].viewWith(config);
				view.initialize({ foo: 3 });
				assert.throws(() => {
					view.root = testObject;
				}, validateUsageError("Tree does not conform to schema: Field_NodeTypeNotAllowed"));
			});

			it("can't be set", () => {
				const provider = new TestTreeProviderLite(1);

				const config = new TreeViewConfiguration({
					schema: TestObject,
				});
				const view = provider.trees[0].viewWith(config);
				view.initialize({ foo: 3 });
				provider.synchronizeMessages();
				assert.throws(() => {
					view.root.foo = "test";
				}, validateUsageError("Tree does not conform to schema: Field_NodeTypeNotAllowed"));
			});
		});

		describe("in maps", () => {
			it("typing", () => {
				const allowedTypes = SchemaFactoryAlpha.types([
					SchemaFactoryAlpha.number,
					SchemaFactoryAlpha.staged(SchemaFactoryAlpha.string),
				]);

				allowUnused<requireAssignableTo<typeof allowedTypes, AllowedTypes>>();
				allowUnused<
					requireAssignableTo<
						typeof allowedTypes,
						readonly [typeof SchemaFactoryAlpha.number, typeof SchemaFactoryAlpha.string]
					>
				>();

				allowUnused<requireAssignableTo<typeof allowedTypes, AnnotatedAllowedTypes>>();
				allowUnused<
					requireAssignableTo<
						typeof allowedTypes,
						UnannotateAllowedTypesList<
							readonly [typeof SchemaFactoryAlpha.number, typeof SchemaFactoryAlpha.string]
						>
					>
				>();
				allowUnused<
					requireAssignableTo<
						typeof allowedTypes,
						AnnotatedAllowedTypes<
							readonly [
								AnnotatedAllowedType<typeof SchemaFactoryAlpha.number>,
								AnnotatedAllowedType<typeof SchemaFactoryAlpha.string>,
							]
						>
					>
				>();

				allowUnused<
					requireAssignableTo<
						typeof allowedTypes,
						AllowedTypesFull<
							readonly [
								AnnotatedAllowedType<typeof SchemaFactoryAlpha.number>,
								AnnotatedAllowedType<typeof SchemaFactoryAlpha.string>,
							]
						>
					>
				>();

				{
					type InField = InsertableTreeFieldFromImplicitField<typeof allowedTypes>;
					allowUnused<requireAssignableTo<InField, number | string>>();
					allowUnused<requireAssignableTo<number | string, InField>>();
				}

				{
					type InAllowedTypes = InsertableTreeNodeFromImplicitAllowedTypes<
						typeof allowedTypes
					>;
					allowUnused<requireAssignableTo<InAllowedTypes, number | string>>();
					allowUnused<requireAssignableTo<number | string, InAllowedTypes>>();
				}

				{
					type InAllowedTypes = InsertableTreeNodeFromAllowedTypes<typeof allowedTypes>;
					allowUnused<requireAssignableTo<InAllowedTypes, number | string>>();
					allowUnused<requireAssignableTo<number | string, InAllowedTypes>>();
				}

				allowUnused<requireFalse<IsUnion<typeof allowedTypes>>>();
			});

			class TestMap extends schemaFactory.mapAlpha(
				"TestMap",
				SchemaFactoryAlpha.types([
					SchemaFactoryAlpha.number,
					SchemaFactoryAlpha.staged(SchemaFactoryAlpha.string),
				]),
			) {}

			it("are permitted when unhydrated", () => {
				const testMap = new TestMap({ foo: "test" });
				assert.equal(testMap.get("foo"), "test");
				testMap.set("foo", 42);
				assert.equal(testMap.get("foo"), 42);
				testMap.set("foo", "test");
				assert.equal(testMap.get("foo"), "test");
			});

			it("can't be hydrated", () => {
				const testMap = new TestMap({ foo: "test" });
				const provider = new TestTreeProviderLite(1);

				const config = new TreeViewConfiguration({
					schema: TestMap,
				});
				const view = provider.trees[0].viewWith(config);
				view.initialize({ foo: 3 });
				assert.throws(() => {
					view.root = testMap;
				}, validateUsageError("Tree does not conform to schema: Field_NodeTypeNotAllowed"));
			});

			it("can't be set", () => {
				const provider = new TestTreeProviderLite(1);

				const config = new TreeViewConfiguration({
					schema: TestMap,
				});
				const view = provider.trees[0].viewWith(config);
				view.initialize({});
				provider.synchronizeMessages();
				assert.throws(() => {
					view.root.set("foo", "test");
				}, validateUsageError("Tree does not conform to schema: Field_NodeTypeNotAllowed"));
			});
		});

		describe("in records", () => {
			class TestRecord extends schemaFactory.recordAlpha(
				"TestRecord",
				SchemaFactoryAlpha.types([
					SchemaFactoryAlpha.number,
					SchemaFactoryAlpha.staged(SchemaFactoryAlpha.string),
				]),
			) {}

			it("are permitted when unhydrated", () => {
				const testRecord = new TestRecord({ foo: "test" });
				assert.equal(testRecord.foo, "test");
				testRecord.foo = 42;
				assert.equal(testRecord.foo, 42);
				testRecord.foo = "test";
				assert.equal(testRecord.foo, "test");
			});

			it("can't be hydrated", () => {
				const testRecord = new TestRecord({ foo: "test" });
				const provider = new TestTreeProviderLite(1);

				const config = new TreeViewConfiguration({
					schema: TestRecord,
				});
				const view = provider.trees[0].viewWith(config);
				view.initialize({ foo: 3 });
				assert.throws(() => {
					view.root = testRecord;
				}, validateUsageError("Tree does not conform to schema: Field_NodeTypeNotAllowed"));
			});

			it("can't be set", () => {
				const provider = new TestTreeProviderLite(1);

				const config = new TreeViewConfiguration({
					schema: TestRecord,
				});
				const view = provider.trees[0].viewWith(config);
				view.initialize({});
				provider.synchronizeMessages();
				assert.throws(() => {
					view.root.foo = "test";
				}, validateUsageError("Tree does not conform to schema: Field_NodeTypeNotAllowed"));
			});
		});

		describe("in arrays", () => {
			/**
			 * Allows numbers, and staged to allow strings.
			 */
			class TestArray extends schemaFactory.arrayAlpha(
				"TestArray",
				SchemaFactoryAlpha.types([
					SchemaFactoryAlpha.number,
					SchemaFactoryAlpha.staged(SchemaFactoryAlpha.string),
				]),
			) {}

			it("are permitted when unhydrated", () => {
				const testArray = new TestArray(["test"]);
				testArray.insertAtEnd("test");
				assert.deepEqual(Array.from(testArray.values()), ["test", "test"]);
				testArray.insertAtEnd(5);
				assert.deepEqual(Array.from(testArray.values()), ["test", "test", 5]);
			});

			it("can't be hydrated", () => {
				const testArray = new TestArray(["test"]);
				const provider = new TestTreeProviderLite(1);

				const config = new TreeViewConfiguration({
					schema: TestArray,
				});
				const view = provider.trees[0].viewWith(config);
				view.initialize([]);
				assert.throws(
					() => {
						view.root = testArray;
					},
					validateUsageError(/Tree does not conform to schema/),
				);
			});

			it("can't be inserted", () => {
				const provider = new TestTreeProviderLite(1);

				const config = new TreeViewConfiguration({
					schema: TestArray,
				});
				const view = provider.trees[0].viewWith(config);
				view.initialize([]);
				provider.synchronizeMessages();
				assert.throws(
					() => {
						view.root.insertAtEnd("test");
					},
					validateUsageError(/Tree does not conform to schema/),
				);
			});

			it("can't be inserted, deeper", () => {
				const provider = new TestTreeProviderLite(1);

				class Root extends schemaFactory.objectAlpha("TestObject", {
					foo: TestArray,
				}) {}

				const config = new TreeViewConfiguration({
					schema: Root,
				});
				const view = provider.trees[0].viewWith(config);
				view.initialize({
					foo: [],
				});
				provider.synchronizeMessages();

				const x = (
					view as SchematizingSimpleTreeView<typeof Root>
				).checkout.storedSchema.nodeSchema.get(brand(Root.identifier));

				const f = x?.getFieldSchema(EmptyKey);

				assert.throws(
					() => {
						view.root.foo.insertAtEnd("test");
					},
					validateUsageError(/Tree does not conform to schema/),
				);
			});

			it("can't be inserted, even when type is referenced", () => {
				const provider = new TestTreeProviderLite(1);

				class Root extends schemaFactory.objectAlpha("TestObject", {
					foo: TestArray,
					// Reference to string schema, so the node schema can be found.
					bar: schemaFactory.string,
				}) {}

				const config = new TreeViewConfiguration({
					schema: Root,
				});
				const view = provider.trees[0].viewWith(config);
				view.initialize({
					foo: [],
					bar: "x",
				});
				provider.synchronizeMessages();

				const x = (
					view as SchematizingSimpleTreeView<typeof Root>
				).checkout.storedSchema.nodeSchema.get(brand(Root.identifier));

				const f = x?.getFieldSchema(EmptyKey);

				assert.throws(
					() => {
						view.root.foo.insertAtEnd("test");
					},
					validateUsageError(/Tree does not conform to schema/),
				);
			});

			it("can't be moved in from another field", () => {
				const provider = new TestTreeProviderLite(1);

				const config = new TreeViewConfiguration({
					schema: schemaFactory.objectAlpha("TestObject", {
						foo: TestArray,
						bar: schemaFactory.arrayAlpha("StringArray", schemaFactory.string),
					}),
				});
				const view = provider.trees[0].viewWith(config);
				view.initialize({
					foo: [],
					bar: ["test"],
				});
				provider.synchronizeMessages();
				assert.throws(() => {
					view.root.foo.moveToEnd(0, view.root.bar);
				}, validateUsageError(
					"Type com.fluidframework.leaf.string in source sequence is not allowed in destination's stored schema: this would likely require upgrading the document to permit a staged schema.",
				));
			});
		});
	});
});

// kind based narrowing example
function getKeys(node: TreeNode & WithType<string, NodeKind.Array>): number[];
function getKeys(node: TreeNode & WithType<string, NodeKind.Map | NodeKind.Object>): string[];
function getKeys(node: TreeNode): string[] | number[];
function getKeys(node: TreeNode): string[] | number[] {
	const schema = Tree.schema(node);
	switch (schema.kind) {
		case NodeKind.Array: {
			const arrayNode = node as TreeArrayNode;
			const keys: number[] = [];
			for (let index = 0; index < arrayNode.length; index++) {
				keys.push(index);
			}
			return keys;
		}
		case NodeKind.Map:
			return [...(node as TreeMapNode).keys()];
		case NodeKind.Object:
			return Object.keys(node);
		default:
			throw new Error("Unsupported Kind");
	}
}
